import { Brackets } from "typeorm";
import { GraphQLScalarType } from 'graphql';

export const convertFilterParameters = <T>(parameters?: IFilter<T>, customFields?: Map<string, GraphqlFilterFieldMetadata>, options?: IFilterDecoratorParams) => {
    // For tests purposes and GlobalPipes like ValidationPipe that uses class-transformer to transform object to the class. 
    // If you provide Brackets instead of object to the decorator, it will use your brackets without processing it.
    if(Object.keys(parameters).length<=0) return undefined;
    if ((parameters as any)?.whereFactory) return parameters;
    return new Brackets((qb) => {
        if (parameters == null) {
            return;
        }
        const clonnedParams = { ...parameters };
        delete clonnedParams.and;
        delete clonnedParams.or;
        if (parameters?.and) {
            qb.andWhere(
                new Brackets((andBracketsQb) => {
                    for (const op of parameters?.and) {
                        const andParameters = recursivelyTransformComparators(op, customFields, options?.sqlAlias);
                        if (andParameters?.length) {
                            for (const query of andParameters) {
                                andBracketsQb.andWhere(query[0], query[1]);
                            }
                        }
                    }
                })
            )
        }
        if (parameters?.or) {
            qb.orWhere(
                new Brackets((orBracketsQb) => {
                    for (const op of parameters?.or) {
                        const orParameters = recursivelyTransformComparators(op, customFields, options?.sqlAlias);
                        if (orParameters?.length) {
                            for (const query of orParameters) {
                                orBracketsQb.orWhere(query[0], query[1]);
                            }
                        }
                    }
                })
            )
        }
        const basicParameters = recursivelyTransformComparators(clonnedParams, customFields, options?.sqlAlias);
        if (basicParameters) {
            qb.andWhere(
                new Brackets((basicParametersQb) => {
                    for (const query of basicParameters) {
                        basicParametersQb.andWhere(query[0], query[1]);
                    }
                })
            )
        }
    });
}


interface Type<T> extends Function {
    new(...args: any[]): T;
}
type GqlTypeReference<T = any> =
    | Type<T>
    | GraphQLScalarType
    | Function
    | object
    | symbol;
type ReturnTypeFuncValue = GqlTypeReference | [GqlTypeReference];
type ReturnTypeFunc<T extends ReturnTypeFuncValue = any> = (
    returns?: void,
) => T;
interface FilterFieldOptions {
    /** Left side of conditional sql expression. 
     * Examples: 
     * 1. Select * from task t where t.title ilike '%test%'. Statement t.title is the left side of a statement 
     * 2. Select * from user u where concat(u.fname, ' ', u.lname) ilike %alex%. Statement concat(u.fname, ' ', u.lname) is the left side of the conditional statement*/
    sqlExp?: string;
}
interface GraphqlFilterFieldMetadata extends FilterFieldOptions {
    name: string;
    typeFn: ReturnTypeFunc;
}
interface IFilterDecoratorParams {
    sqlAlias?: string;
    name?: string;
}

type IFilterField<T> = {
    [K in keyof T]: {
        eq: T[K],
        neq: T[K],
        gt: T[K],
        gte: T[K],
        lt: T[K],
        lte: T[K],
        in: T[K],
        like: T[K],
        notlike: T[K],
        between: T[K],
        notbetween: T[K],
        null: T[K],
    };
}

export interface IFilter<T>  {
    and: IFilterField<T>[];
    or: IFilterField<T>[];
}

enum OperationQuery {
    eq = 'eq',
    neq = 'neq',
    gt = 'gt',
    gte = 'gte',
    lt = 'lt',
    lte = 'lte',
    in = 'in',
    notin = 'notin',
    like = 'like',
    notlike = 'notlike',
    between = 'between',
    notbetween = 'notbetween',
    null = 'null',
}
const FILTER_OPERATION_PREFIX = process.env.FILTER_OPERATION_PREFIX || undefined;
const convertArrayOfStringIntoStringNumber = (array: string[]) => {
    if (!array.length) return '';
    const sortedArray = array.sort();
    const stringArray = Array.from(sortedArray.join(''));
    const charSum = stringArray.reduce((sum, x) => (sum + x.charCodeAt(0)), 0);
    return `${charSum}${stringArray.length}${sortedArray.length}`;
}


const recursivelyTransformComparators = (object: Record<string, any>, extendedParams?: Map<string, GraphqlFilterFieldMetadata>, sqlAlias?: string) => {
    if (!object || !Object.entries(object).length) return null;
    const typeormWhereQuery = [];
    for (const [key, value] of Object.entries(object)) {
        if (typeof value === "object") {
            const operators = Object.entries(
                value as Record<string, any>
            );
            if (operators.length > 1) {
                throw new Error('Inside filter statement should be only one condition operator for each attribute');
            }
            for (const [innerKey, innerValue] of operators) {
                const operatorKey = innerKey.replace(FILTER_OPERATION_PREFIX, '');
                if (extendedParams.has(key)) {
                    const field = extendedParams.get(key);
                    const rightExpression = field.sqlExp ? field.sqlExp : (sqlAlias ? `${sqlAlias}.${field.name}` : field.name);
                    typeormWhereQuery.push(buildSqlArgument(operatorKey, rightExpression, innerValue));
                } else {
                    const rightExpression = sqlAlias ? `${sqlAlias}.${key}` : key;
                    typeormWhereQuery.push(buildSqlArgument(operatorKey, rightExpression, innerValue));
                }
            }
        }
    }
    return typeormWhereQuery;
}

const buildSqlArgument = (operatorKey: string, field: string, value: any) => {
    let result: any[] = [];
    const argName = `arg_${convertArrayOfStringIntoStringNumber([field])}`
    if (operatorKey === OperationQuery.eq) {
        if (value === null || value === 'null') {
            result = [`${field} is null`];
        } else {
            result = [`${field} = :${argName}`, { [argName]: value }];
        }
    } else if (operatorKey === OperationQuery.neq) {
        if (value === null || value === 'null') {
            result = [`${field} != :${argName}`, { [argName]: value }];
        } else {
            result = [`${field} is not null`];
        }
    } else if (operatorKey === OperationQuery.lt) {
        result = [`${field} < :${argName}`, { [argName]: value }];
    } else if (operatorKey === OperationQuery.lte) {
        result = [`${field} <= :${argName}`, { [argName]: value }];
    } else if (operatorKey === OperationQuery.gt) {
        result = [`${field} > :${argName}`, { [argName]: value }];
    } else if (operatorKey === OperationQuery.gte) {
        result = [`${field} >= :${argName}`, { [argName]: value }];
    } else if (operatorKey === OperationQuery.like) {
        result = [`${field}::varchar ilike :${argName}::varchar`, { [argName]: value }];
    } else if (operatorKey === OperationQuery.notlike) {
        result = [`${field}::varchar not ilike :${argName}::varchar`, { [argName]: value }];
    } else if (operatorKey === OperationQuery.between) {
        result = [`${field} between :${argName}1 and :${argName}2`, { [`${argName}1`]: value[0], [`${argName}2`]: value[1] }];
    } else if (operatorKey === OperationQuery.notbetween) {
        result = [`${field} not between :${argName}1 and :${argName}2`, { [`${argName}1`]: value[0], [`${argName}2`]: value[1] }];
    } else if (operatorKey === OperationQuery.in) {
        result = [`${field} in (:...${argName})`, { [argName]: value }];
    } else if (operatorKey === OperationQuery.notin) {
        result = [`${field} not in (:...${argName})`, { [argName]: value }];
    } else if (operatorKey === "any") {
        result = [`${field} any (:${argName})`, { [argName]: value }];
    } else if (operatorKey === OperationQuery.null) {
        if (value === 'true' || value === true) {
            result = [`${field} is null`];
        } else {
            result = [`${field} is not null`];
        }
    }

    return result;
}